/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.lvyh.lightframe.common.ext;

import com.lvyh.lightframe.common.Holder;
import com.lvyh.lightframe.common.constant.RpcConstants;
import com.lvyh.lightframe.common.util.ClassUtils;
import com.lvyh.lightframe.common.exception.RpcRuntimeException;
import com.lvyh.lightframe.common.util.ExceptionUtils;
import com.lvyh.lightframe.common.util.StringUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * An extensible interface class, corresponding to a loader
 */
public class ExtensionLoader<T> {
    private final static Logger LOGGER = LoggerFactory.getLogger(ExtensionLoader.class);

    /**
     * The name of the currently loaded interface class
     */
    protected final Class<T> interfaceClass;

    /**
     * Interface name
     */
    protected final String interfaceName;

    protected final Extensible extensible;

    /**
     * All loaded implementation classes {"alias":ExtensionClass}
     */
    protected final ConcurrentMap<String, ExtensionClass<T>> all;

    /**
     * Load listener
     */
    protected final List<ExtensionLoaderListener<T>> listeners;

    private final Map<String, Holder<T>> cachedInstances = new ConcurrentHashMap();

    protected ExtensionLoader(Class<T> interfaceClass) {
        this(interfaceClass, null);
    }

    protected ExtensionLoader(Class<T> interfaceClass, ExtensionLoaderListener<T> listener) {
        Extensible extensible = checkAndGet(interfaceClass);

        this.interfaceClass = interfaceClass;
        this.interfaceName = ClassUtils.getTypeStr(interfaceClass);
        this.listeners = new ArrayList<>();
        if (listener != null) {
            listeners.add(listener);
        }

        this.extensible = extensible;
        this.all = new ConcurrentHashMap<String, ExtensionClass<T>>();
        loadFromFile(RpcConstants.EXTENSION_LOAD_PATH);
    }


    private static <T> Extensible checkAndGet(Class<T> interfaceClass) {
        if (interfaceClass == null) {
            throw new IllegalArgumentException(interfaceClass.getName() + " can not be null.");
        }
        if (!(interfaceClass.isInterface() || Modifier.isAbstract(interfaceClass.getModifiers()))) {
            throw new IllegalArgumentException(interfaceClass.getName() + " is not an interface or abstract class.");
        }

        Extensible extensible = interfaceClass.getAnnotation(Extensible.class);
        if (extensible == null) {
            throw new IllegalArgumentException(interfaceClass.getName() + " does not has annotation:" + Extensible.class.getName() + ".");
        }
        return extensible;
    }

    protected synchronized void loadFromFile(String path) {
        LOGGER.info("Loading spi of extensible {} from path: {}", interfaceName, path);
        String fullFileName = path + interfaceName;
        try {
            ClassLoader classLoader = ClassUtils.getClassLoader(getClass());
            loadFromClassLoader(classLoader, fullFileName);
        } catch (Throwable t) {
            LOGGER.error("Failed to load spi of extensible " + interfaceName + " from path:" + fullFileName, t);
        }
    }

    protected void loadFromClassLoader(ClassLoader classLoader, String fullFileName) throws Throwable {
        Enumeration<URL> urls = classLoader != null ? classLoader.getResources(fullFileName) : ClassLoader.getSystemResources(fullFileName);
        // Multiple files may exist
        if (urls != null) {
            while (urls.hasMoreElements()) {
                // Read a file
                URL url = urls.nextElement();
                LOGGER.info("Loading spi of extensible {} from classloader: {} and file: {}",
                        interfaceName, classLoader, url);
                BufferedReader reader = null;
                try {
                    reader = new BufferedReader(new InputStreamReader(url.openStream(), "UTF-8"));
                    String line;
                    while ((line = reader.readLine()) != null) {
                        readLine(url, line);
                    }
                } catch (Throwable t) {
                    LOGGER.error("Failed to load spi of extensible " + interfaceName
                            + " from classloader: " + classLoader + " and file:" + url, t);
                } finally {
                    if (reader != null) {
                        reader.close();
                    }
                }
            }
        }
    }

    protected void readLine(URL url, String line) {
        String[] aliasAndClassName = parseAliasAndClassName(line);
        if (aliasAndClassName == null || aliasAndClassName.length != 2) {
            return;
        }
        String alias = aliasAndClassName[0];
        String className = aliasAndClassName[1];
        Class tmp;
        try {
            tmp = ClassUtils.forName(className, false);
        } catch (Throwable e) {
            LOGGER.error("spi {} of extensible {} is disabled, cause by: {}",
                    className, interfaceName, ExceptionUtils.toShortString(e, 2));
            return;
        }

        loadExtension(alias, tmp, StringUtil.toString(url), className);
    }

    private void loadExtension(String alias, Class loadedClazz, String location, String className) {
        if (!interfaceClass.isAssignableFrom(loadedClazz)) {
            throw new IllegalArgumentException("Error when load spi of extensible " + interfaceName +
                    " from file:" + location + ", " + className + " is not subtype of interface.");
        }
        Class<? extends T> implClass = (Class<? extends T>) loadedClazz;

        // Check for extensible identity
        Spi extension = implClass.getAnnotation(Spi.class);
        if (extension == null) {
            throw new IllegalArgumentException("Error when load spi of extensible " + interfaceName +
                    " from file:" + location + ", " + className + " must add annotation @Spi.");
        } else {
            String aliasInCode = extension.value();
            if (StringUtils.isBlank(aliasInCode)) {
                // The @SPI tag is not configured for the extension implementation class
                throw new IllegalArgumentException("Error when load spi of extensible " + interfaceClass +
                        " from file:" + location + ", " + className + "'s alias of @Spi is blank");
            }
            if (alias == null) {
                //There is no configuration in the SPI file, use the config in code
                alias = aliasInCode;
            } else {
                // The configuration in SPI file is inconsistent with that in code
                if (!aliasInCode.equals(alias)) {
                    throw new IllegalArgumentException("Error when load spi of extensible " + interfaceName +
                            " from file:" + location + ", aliases of " + className + " are " +
                            "not equal between " + aliasInCode + "(code) and " + alias + "(file).");
                }
            }

        }

        // Check whether there are extensions with the same name, and override other extensions with the same name
        ExtensionClass<T> extensionClass = buildClass(implClass, alias);
        if (extensionClass != null) {
            loadSuccess(alias, extensionClass);
        }
    }

    private ExtensionClass<T> buildClass(Class<? extends T> implClass, String alias) {
        ExtensionClass<T> extensionClass = new ExtensionClass<T>(implClass, alias);
        return extensionClass;
    }

    private void loadSuccess(String alias, ExtensionClass<T> extensionClass) {
        if (listeners != null) {
            for (ExtensionLoaderListener<T> listener : listeners) {
                try {
                    listener.onLoad(extensionClass);
                } catch (Exception e) {
                    LOGGER.error("Error when load spi of extensible " + interfaceClass + " with alias: " + alias + ".", e);
                }
            }

        }
        all.put(alias, extensionClass);
    }

    protected String[] parseAliasAndClassName(String line) {
        if (StringUtils.isBlank(line)) {
            return null;
        }
        line = line.trim();
        String alias = null;
        String className;
        int i = line.indexOf('=');
        if (i > 0) {
            alias = line.substring(0, i);
            className = line.substring(i + 1);
        } else {
            className = line;
        }
        if (className.length() == 0) {
            return null;
        }
        return new String[]{alias, className};
    }

    /**
     * Return all extended classes
     */
    public ConcurrentMap<String, ExtensionClass<T>> getAllExtensions() {
        return all;
    }

    /**
     * Finding extension classes based on service aliases
     *
     * @param alias Extended alias
     * @return Extension class object
     */
    public ExtensionClass<T> getExtensionClass(String alias) {
        return all == null ? null : all.get(alias);
    }

    /**
     * Get an instance
     *
     * @param alias
     */
    public T getExtension(String alias) {
        ExtensionClass<T> extensionClass = getExtensionClass(alias);
        if (extensionClass == null) {
            throw new RpcRuntimeException("extension not found, alias:" + alias);
        } else {
            return getExtInstance(extensionClass, alias, null, null);
            //return extensionClass.getExtInstance();
        }
    }

    private T createExtension(ExtensionClass<T> extensionClass) {
        return extensionClass.getExtInstance();
    }

    private T getExtInstance(ExtensionClass<T> extensionClass, String name, Class[] argTypes, Object[] args) {
        Holder<T> holder = cachedInstances.get(name);
        if (holder == null) {
            cachedInstances.putIfAbsent(name, new Holder<T>());
            holder = cachedInstances.get(name);
        }

        T instance = holder.get();
        if (instance != null) {
            return instance;
        }

        synchronized (holder) {
            instance = holder.get();
            if (instance != null) {
                return instance;
            }

            if (argTypes != null && args != null) {
                instance = createExtension(extensionClass, argTypes, args);
            } else
                instance = createExtension(extensionClass);
            holder.set(instance);
        }
        return instance;
    }


    /**
     * Get an instance
     *
     * @param alias
     * @param argTypes Parameter types required for extension initialization
     * @param args     Parameters required for extension initialization
     * @return ext instance
     */
    public T getExtension(String alias, Class[] argTypes, Object[] args) {
        ExtensionClass<T> extensionClass = getExtensionClass(alias);
        if (extensionClass == null) {
            throw new RpcRuntimeException("error spi not found,alias:" + alias);
        } else {
            return getExtInstance(extensionClass, alias, argTypes, args);
            // return extensionClass.getExtInstance(argTypes, args);
        }
    }

    private T createExtension(ExtensionClass<T> extensionClass, Class[] argTypes, Object[] args) {
        return extensionClass.getExtInstance(argTypes, args);
    }

    public void loadExtension(Class loadedClass) {
        if (loadedClass == null) {
            throw new IllegalArgumentException("Can not load spi of null");
        }
        loadExtension(null, loadedClass, "", loadedClass.getName());
    }

    public void addListener(ExtensionLoaderListener<T> listener) {
        synchronized (this) {
            if (!listeners.contains(listener)) {
                this.listeners.add(listener);
                for (ExtensionClass<T> value : all.values()) {
                    try {
                        listener.onLoad(value);
                    } catch (Exception e) {
                        LOGGER.error("Error when notify listener of extensible " + interfaceClass + " with alias: " + value.getAlias() + ".", e);
                    }
                }
            }
        }

    }
}
